<?php
class HtmlQuery {
	private $_current;

	protected function setQueryLookup(&$current) {
		$this->_current = $current;
	}
	
	protected function parse_selector($selector) {
		$order = array();
		
		$patterns = array(
			"/^#((?:[\wa-z\-]|\\.)+)/i", // id
			"/^\\.((?:[\wa-z\-]|\\.)+)/i", // class
			"/^((?:[\wa-z\*\-]|\\.)+)/i",	// tag
			"/^\\[\\s*((?:[\\wa-z\\-]|\\\\.)+)\\s*(?:(\\S?=)\\s*(?:([\'\"])(.*?)\\3|(#?(?:[\\wa-z\\-]|\\\\.)*)|)|)\\s*\\]/i", // attr
			"/^\\:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/i", //child
			"/^\\:(nth|eq|gt|lt|first|last|even|odd)(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/" // pos
		);
		
		$depth = 0;
		$slen = strlen($selector);
		while($slen > 0 && $depth < 10) {
			for($i = 0; $i < 6; ++$i) {
				if(preg_match($patterns[$i], $selector, $matches, PREG_OFFSET_CAPTURE)) {
					$params = array($i);
					if($i < 3) {
						array_push($params, $matches[1][0]);
					} else if($i == 3) {
						array_push($params, $matches[1][0], $matches[2][0], $matches[4][0]);
					} else if($i >= 4) {
						$tmp = null;
						if(isset($matches[2])) {
							$tmp = $matches[2][0];
						}
						array_push($params, $matches[1][0], $tmp);
					}
					$order []= $params;
					// shorten selector
					$len = strlen($matches[0][0]);
					$selector = trim(substr($selector, intval($matches[0][1]) + $len, $slen-$len));
				}
			}
			$depth++;
			$slen = strlen($selector);
		}
		return $order;
	}
	
	protected function find_r($selector, &$group, $current) {
		$order = $this->parse_selector($selector);
		
		$evaluate = function($expr, $n) {
			$expr = str_replace("n", $n, $expr);
			$val = intval($n);
			if(strstr($expr, '-')) {
				list($a, $b) = explode('-', $expr);
				$val = intval($a) - intval($b);
			}
			return $val;
		};
	
		$callbacks = array(
			function($element, $id) { // id
				echo "id: $id\r\n<br/>";
				return null;
			},
			function($element, $class) { // class
				echo "class: $class\r\n<br/>";
				return null;
			},
			function($ele, $tag) use(&$callbacks) { // tag
				$matches = array();
				$children = $ele->getChildren();
				$max = count($children);
				for($i = 0; $i < $max; ++$i) {
					if($children[$i]->getTag() == $tag) {
						$matches []= $children[$i];
					}
					$ret = call_user_func($callbacks[2], $children[$i], $tag);
					if($ret != null) {
						$matches = array_merge($matches, $ret);
					}
				}
				if(count($matches) > 0) {
					return $matches;
				}
				return null;
			},
			function($ele, $attr, $operator, $value) use(&$callbacks) { //  attr
				$matches = array();
				$children = $ele->getChildren();
				$max = count($children);
				for($i = 0; $i < $max; ++$i) {
					switch($operator) {
						case "=":
						if($children[$i]->getAttribute($attr) == $value) {
							$matches []= $children[$i];
						}
						break;
					}
					$ret = call_user_func($callbacks[3], $children[$i], $attr, $operator, $value);
					if($ret != null) {
						$matches = array_merge($matches, $ret);
					}
				}
				if(count($matches) > 0) {
					return $matches;
				}
				return null;
			},
			function($ele, $switch, $val) use(&$callbacks, &$evaluate) { //  child
				$matches = array();
				switch($switch) {
					case "first":
						$matches []= $ele->firstChild();
					break;
					case "last":
						$matches []= $ele->lastChild();
					break;
					case "nth":
						$children = $ele->getChildren();
						$max = count($children);
						if($val == '2n+1') { $val = 'odd'; }
						if($val == '2n') { $val = 'even'; }
						if($val == 'odd') {
							for($i = 0; $i < $max; ++$i) {
								if($i % 2 == 1) {
									$matches []= $children[$i];
								}
							}
						} else if ($val == 'even') {
							for($i = 0; $i < $max; ++$i) {
								if($i % 2 == 0) {
									$matches []= $children[$i];
								}
							}
						} else {
							$idx = $evaluate($val, $max);
							if(isset($children[$idx])) {
								$matches []= $children[$idx];
							}
						}
					break;
					case "only":
						$children = $ele->getChildren();
						if(count($children) == 1) {
							$matches []= $children[0];
						}
					break;
				}
				if(count($matches) > 0) {
					return $matches;
				}
				return null;
			},
			function($_matches, $switch, $val) use(&$evaluate) { // pos
				$matches = array();
				switch($switch) {
					case "first":
						$matches []= $_matches[0];
					break;
					case "last":
						$max = count($_matches);
						$matches []= $_matches[$max-1];
					break;
					case "even":
						$max = count($_matches);
						for($i = 0; $i < $max; ++$i) {
							if($i % 2 == 0) {
								$matches []= $_matches[$i];
							}
						}
					break;
					case "odd":
						$max = count($_matches);
						for($i = 0; $i < $max; ++$i) {
							if($i % 2 == 1) {
								$matches []= $_matches[$i];
							}
						}
					break;
					case "nth":
						$max = count($_matches);
						if($val == '2n+1') { $val = 'odd'; }
						if($val == '2n') { $val = 'even'; }
						if($val == 'odd') {
							for($i = 0; $i < $max; ++$i) {
								if($i % 2 == 1) {
									$matches []= $_matches[$i];
								}
							}
						} else if ($val == 'even') {
							for($i = 0; $i < $max; ++$i) {
								if($i % 2 == 0) {
									$matches []= $_matches[$i];
								}
							}
						} else {
							$idx = $evaluate($val, $max);
							if(isset($_matches[$idx])) {
								$matches []= $_matches[$idx];
							}
						}
					break;
					case "eq":
						$max = count($_matches);
						$idx = $evaluate($val, $max);
						if(isset($_matches[$idx])) {
							$matches []= $_matches[$idx];
						}
					break;
					case "lt":
						$max = count($_matches);
						
						$idx = $evaluate($val, $max);
						for($i = 0; $i < $max; ++$i) {
							if($i < $idx) {
								$matches []= $_matches[$i];
							}
						}
					break;
					case "gt":
						$max = count($_matches);
						$idx = $evaluate($val, $max);
						for($i = 0; $i < $max; ++$i) {
							if($i > $idx) {
								$matches []= $_matches[$i];
							}
						}
					break;
				}
				if(count($matches) > 0) {
					return $matches;
				}
				return null;
			}
		);
		
		$matches = array();
		$_matches = array();
		$max = count($order);
		for($i = 0; $i < $max; ++$i) {
			
			$clbkid = array_shift($order[$i]);
			$mmax = count($matches);
			if($mmax > 0) {
				for($j = 0; $j < $mmax; ++$j) {
					if($clbkid < 5) {
					$params = array_merge(array($matches[$j]), $order[$i]);
					}else{
						$params = array_merge(array($matches), $order[$i]);
					}
					$ret = call_user_func_array($callbacks[$clbkid], $params);
					if($ret != null) {
						$_matches = array_unique(array_merge($_matches, $ret), SORT_REGULAR);
					}
				}
				$matches = $_matches;
				$_matches = array();
			} else {
				$params = array_merge(array($current), $order[$i]);
				$ret = call_user_func_array($callbacks[$clbkid], $params);
				if($ret != null) {
					$matches = array_unique(array_merge($matches, $ret), SORT_REGULAR);
				}
			}
		}
		
		foreach($matches as $ele) {
			$group->Add($ele);
		}
	}
	
	public function Find($query) {
		$selectors = explode(",", $query);
		$group = new HtmlElementGroup();
		foreach($selectors as $selector) {
			$this->find_r($selector, $group, $this->_current);
		}
		return $group;
	}
};

class HtmlElementGroup extends HtmlQuery implements Iterator {
	private $position;
	private $elements;
	
	function __construct() {
		$this->elements = array();
		$this->position = 0;
	}
	
	function Add(&$ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			if($this->elements[$i] == $ele) { 
				return;
			}
		}
		$this->elements [] = $ele;
	}
	
	function Remove($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->Remove($ele);
		}
	}
	
	function RemoveAt($index) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->RemoveAt($index);
		}
	}
	
	function insertAfter($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->insertAfter($ele);
		}
	}
	
	function insertBefore($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->insertBefore($ele);
		}
	}
	
	function After($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->After($ele);
		}
	}
	
	function Before($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->Before($ele);
		}
	}
	
	function clearAttributes() {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->clearAttributes();
		}
	}
	
	function clearChildren() {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->clearChildren();
		}
	}
	
	function getAttribute($attr) {
		return $this->elements[0]->getAttribute($attr);
	}

	function setAttribute($attr, $value) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->setAttribute($attr, $value);
		}
	}
	
	function setAttributes($attrs) {
		foreach($attrs as $attr=>$value) {
			$max = count($this->elements);
			for($i = 0; $i < $max; ++$i) {
				$this->elements[$i]->setAttribute($attr, $value);
			}
		}
	}
	
	function setText($txt) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->setText($txt);
		}
	}
	
	function getText() {
		$text = array();
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$text []= $this->elements[$i]->getText();
		}
		return explode(null, $text);
	}
	
	function setHtml($html) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->setHtml($html);
		}
	}
	
	function getHtml() {
		return $this->elements[0]->getHtml();
	}
	
	function Append($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->Append($ele);
		}
	}
	
	function Prepend($ele) {
		$max = count($this->elements);
		for($i = 0; $i < $max; ++$i) {
			$this->elements[$i]->Prepend($ele);
		}
	}

	/*
		& iterator methods
	*/
	function rewind() {
		$this->position = 0;
	}

	function current() {
		return $this->elements[$this->position];
	}

	function key() {
		return $this->position;
	}

	function next() {
		++$this->position;
	}

	function valid() {
		return isset($this->elements[$this->position]);
	}
};

define('HtmlTypeElement', 1);
define('HtmlTypeComment', 2);
define('HtmlTypeText',    3);
define('HtmlTypeRoot',    4);

class HtmlElement extends HtmlQuery {
	private $tag;
	private $closure;
	private $attributes;
	private $children;
	private $parent;
	private $namespace;
	private $type;
	
	public function __construct($data = null) {
		$this->tag = null;
		$this->closure = true;
		$this->attributes = array();
		$this->children = array();
		$this->parent = null;
		$this->namespace = null;
		$this->type = HtmlTypeElement;
		
		if($data != null) {
			$datlen = strlen($data);
			if($data[0] == '<' && $data[$datlen-1] == '>' && $datlen >= 3) {
				$this->parseHtml($data);
			} else if(substr($data, 0, 4) == 'http') {
				$this->parseHtml(file_get_contents($data));
			} else if(substr($data, 0, 4) == 'path') {
				$this->parseHtml(file_get_contents(substr($data, 7, strlen($data)-7)));
			} else {
				$this->tag = $data;
			}
		}
		$this->setQueryLookup($this);
	}
	
	static function CreateTextElement($text) {
		$ele = new HtmlElement();
		$ele->type = HtmlTypeText;
		$ele->attributes['value'] = $text;
		return $ele;
	}
	
	static function CreateCommentElement($comment) {
		$ele = new HtmlElement();
		$ele->type = HtmlTypeComment;
		$ele->attributes['value'] = $comment;
		return $ele;
	}
	
	public function indexOf() {
		if($this->parent != null) {
			$parent = $this->parent;
			$max = sizeof($parent->children);
			for($i = 0; $i < $max; ++$i) {
				if($parent->children[$i] == $this) {
					return $i;
				}
			}
		}
		return null;
	}
	
	function hasClass($class) {
		if(isset($this->attributes['class'])) {
			return preg_match("/\s".$class."\s/", $this->attributes['class']);
		}
		return false;
	}
	
	function addClass($class) {
		if(!$this->hasClass($class)) {
			$this->attributes['class'] = trim($this->attributes['class']).' '.$class;
		}
	}
	
	function removeClass($class) {
		if($this->hasClass($class)) {
			$arr = explode(" ", $this->attributes['class']);
			$max = count($arr);
			for($i = 0; $i < $max; ++$i) {
				if($arr[$i] == $class) {
					unset($arr[$i]);
					$i = $max;
				}
			}
			$this->attributes['class'] = implode(" ", $arr);
		}
	}
	
	public function insertAfter($ele, $clone = true) {
		$idx = $ele->indexOf();
		if($clone) {
			$clone = new HtmlElement();
			$clone->Copy($this);
		} else {
			$clone = $this;
		}
		$clone->parent = $ele->parent;
		array_insert($ele->parent->children, $clone, $idx+1);
	}
	
	public function insertBefore($ele, $clone = true) {
		$idx = $ele->indexOf();
		if($clone) {
			$clone = new HtmlElement();
			$clone->Copy($this);
		} else {
			$clone = $this;
		}
		$clone->parent = $ele->parent;
		array_insert($ele->parent->children, $clone, $idx-1);
	}
	
	public function After($ele, $clone = true) {
		$idx = $this->indexOf();
		if($clone) {
			$clone = new HtmlElement();
			$clone->Copy($ele);
		} else {
			$clone = $ele;
		}
		$clone->parent = $this->parent;
		array_insert($this->parent->children, $clone, $idx+1);
	}
	
	public function Before($ele) {
		$idx = $this->indexOf();
		if($clone) {
			$clone = new HtmlElement();
			$clone->Copy($ele);
		} else {
			$clone = $ele;
		}
		$clone->parent = $this->parent;
		array_insert($this->parent->children, $clone, $idx-1);
	}
	
	public function Prepend($element) {
		$element->parent = $this;
		array_unshift($this->children, $element);
	}
	
	public function Append($element) {
		$element->parent = $this;
		$this->children []= $element;
	}
	
	public function setAttributes($attrs) {
		foreach($attrs as $attr=>$value) {
			$this->setAttribute($attr, $value);
		}
	}
	
	function Remove($ele) {
		$max = count($this->children);
		for($i = 0; $i < $max; ++$i) {
			if($this->children[$i] == $ele) {
				unset($this->children[$i]);
			}
		}
		$this->children = array_values($this->children);
	}
	
	function RemoveAt($i) {
		unset($this->children[$i]);
		$this->children = array_values($this->children);
	}
	
	public function setAttribute($key, $value = null) {
		$this->attributes[$key] = $value;
	}
	
	public function getAttribute($key) {
		if(isset($this->attributes[$key])) {
			return $this->attributes[$key];
		} else {
			return null;
		}
	}
	
	public function clearAtttributes() {
		$this->attributes = array();
	}
	
	public function clearChildren() {
		$this->children = array();
	}
	
	protected function parseHtml($html, $useThis = false) {
		$length = strlen($html);
		$char = null;
		
		$inToken = false;
		$previousTokenName = null;
		$tokenString = null;
		$tokenStringLength = 0;
		
		$textStart = 0;
		

		
		$inUnfriendly = false;
		$unfriendlyTokens = array("script", "style");
		$closureTokens = array("comment", "meta", "link", "img", "hr", "br","input","area","base","col","basefont","frame","param");
		
		$attributesPattern = "/((([a-z-_]+)=([a-z-_]+))|(([a-z-_]+)=\"([^\"]+)\")|(([a-z-_]+)='([^']+)')|([a-z-_]+))/i";
		$conditionalPattern = "/\\[([^\\]]+)\\]/";
		
		$elements = array();
		if($useThis) {
			$elements[0] = $this;
		} else {
			$elements[0] = new HtmlElement("tmp");
		}
		$depth = 0;
		
		// next character ignore whitespace
		$nciws = function($html, $length, $i, $search) {
			$ready = false;
			$str = "";
			$strlen = 0;
			$srchlen = strlen($search);
			
			$char = 0;
			$index = $i+1;
			while($index < $length && $strlen < $srchlen) {
				$char = $html[$index];
				if($char != "\t" || $char != "\r" || $char != "\n") {
					if($ready && ctype_alpha($char)) {
						$str .= $char;
						$strlen++;
					} else if(!$ready) {
						if($char != "/" && $char != " ") { return false; }
						if($char == "/") { $ready = true; }
					}
				}
				$index++;
			}
			return $str == $search;
		};
		
		$name = null;
		$string = null;
		
		$start_time = microtime(true);
		for($i = 0; $i < $length; ++$i) {
			$char = $html[$i];
			if($char != "\t" || $char != "\r" || $char != "\n") {
				if($char == '>'  && $inToken && !$inUnfriendly) {
					$inToken = false;
					$name = $string = $namespace = '';
					if($tokenStringLength > 0 && $tokenString[0] == '!') {
						$name = 'comment';
						$string = $tokenString;
					} else {
						$out = explode(' ', $tokenString, 2);
						$name = $out[0];
						$string = '';
						if(stristr($name, ':')) {
							$out = explode(':', $name, 2);
							$name = $out[0];
							if(count($out) > 1) {
								$out = explode(' ', $out[1], 2);
								$namespace = $out[0];
								$string = $out[1];
							}
						}else if(count($out) > 1) {
							$string .= $out[1];
						}
					}

					if($name[0] == '/') {
						$_name = substr($name, 1, strlen($name));
						if($elements[$depth]->tag == $_name) {
							$depth--;
						}
					} else {
						$ele = null;
						if($name=="comment"){
							$ele = HtmlElement::CreateCommentElement($string);
						} else {
							$ele = new HtmlElement($name);
						}
						if($namespace != '') {
							$ele->namespace = $namespace;
						}
						if(preg_match_all($attributesPattern, $string, $matches)) {
							$mc = count($matches[3]);
							for($j = 0; $j < $mc; ++$j) {
								$ele->setAttribute($matches[3][$j], $matches[4][$j]);
							}
							$mc = count($matches[6]);
							for($j = 0; $j < $mc; ++$j) {
								$ele->setAttribute($matches[6][$j], $matches[7][$j]);
							}

							//print_r($matches);
							//echo "\r\n";
						}
						
						$elements[$depth]->Append($ele);
						if(in_array($name, $closureTokens)) {
							$ele->closure = false;
						} else {
							$elements[++$depth] = $ele;
						}
					}
					
					if(in_array($name, $unfriendlyTokens)) {
						$inUnfriendly = true;
					}
					$tokenString = '';
					$tokenStringLength = 0;
					
					$textStart = $i+1;
				}
				
				if($inToken) {
					$tokenString .= $char;
					$tokenStringLength++;
				}
				
				if($inUnfriendly) {
					if($char == '<'  && !$inToken && $nciws($html, $length, $i, $elements[$depth]->tag)) {
						$inToken = true;
						$textLength = ($i)-$textStart;
						if($textLength > 0) {
							$txt = trim(substr($html,$textStart,$textLength));
							$elements[$depth]->Append(HtmlElement::CreateTextElement($txt));
						}
						$tokenString = '';
						$inUnfriendly = false;
					}
				} else {
					if($char == '<'  && !$inToken) {
						$inToken = true;
						$tokenString = '';
						$textLength = ($i)-$textStart;
						if($textLength > 0) {
							$txt = trim(substr($html,$textStart,$textLength));
							$elements[$depth]->Append(HtmlElement::CreateTextElement($txt));
						}
					}
				}
			}
		}
		$end_time = microtime(true);
		if(!$useThis) {
			$this->copyChildren($elements[0]);
		} else {
			$this->Copy($elements[0]);
		}
		echo "<!-- built tree in ".($end_time - $start_time)." seconds -->\r\n";
	}

	public function getText() {
		if($this->type == HtmlTypeText || $this->type == HtmlTypeComment) {
			return $this->attributes['value'];
		} else {
			$text = '';
			for($i = 0; $i < count($this->children); ++$i) {
				$text .= $this->children[$i]->getText();
			}
			return $text;
		}
	}
	
	public function setText($text) {
		if($this->type == HtmlTypeText || $this->type == HtmlTypeComment) {
			$this->attributes['value'] = $text;
		} else {
			$this->clearChildren();
			$this->Append(HtmlElement::CreateTextElement($text));
		}
	}
	
	public function setHtml($html) {
		$this->clearChildren();
		$this->parseHtml($html, true);
	}
	
	public function getHtml() {		
		if($this->type == HtmlTypeElement) {
			// create attributes string
			$attrs = '';
			if(count($this->attributes) > 0) {
				$attrs = array();
				foreach($this->attributes as $key=>$val) {
					if($val == null) {
						$attrs []= "$key";
					} else {
						$attrs []= "$key=\"$val\"";
					}
				}
				$attrs = implode(" ", $attrs);
			}
			
			$ns = '';
			if($this->namespace != null) {
				$ns = ":{$this->namespace}";
			}
		
			if($this->closure) {
				$html = array();
				// append opening tag
				if($this->tag != '') {
					$html []= "<{$this->tag}{$ns}{$attrs}>";
				}
				// append children html
				$max = count($this->children);
				for($i = 0; $i < $max; ++$i) {
					$ele = $this->children[$i];
					$html []= $ele->getHtml();
				}
				if($this->tag != '') {
					$html []= "</{$this->tag}{$ns}>";
				}
				return implode(null, $html);
			} else {
				if($this->tag != '') {
					return "<{$this->tag}{$ns}{$attrs} />";
				} else {
					return null;
				}
			}
		} else if($this->type == HtmlTypeText) {
			return $this->attributes['value'];
		} else if($this->type == HtmlTypeComment) {
			return "<".$this->attributes['value'].">";
		} else if($this->type == HtmlTypeRoot) {
			$html = array();
			// append children html
			$max = count($this->children);
			for($i = 0; $i < $max; ++$i) {
				$ele = $this->children[$i];
				$html []= $ele->getHtml();
			}
			return implode(null, $html);
		}
	}
	
	public function copyChildren($ele, $start = 0, $end = -1) {
		if($end < 0) { $end = count($ele->children); }
		for($i = $start; $i < $end; ++$i) {
			$this->Append($ele->children[$i]);
		}
	}
	
	public function getTag() {
		return $this->tag;
	}
	
	public function Copy($ele) {
		$this->tag = $ele->tag;
		$this->closure = $ele->closure;
		$this->attributes = $ele->attributes;
		$this->children = $ele->children;
		$this->parent = $ele->parent;
		$this->namespace = $ele->namespace;
		$this->type = $ele->type;
	}
	
	public function firstChild() {
		return $this->children[0];
	}
	
	public function lastChild() {
		return $this->children[count($this->children)-1];
	}
	
	public function getChildren() {
		return $this->children;
	}
};

class HtmlDocument extends HtmlElement {
	public function __construct($data = null) {
		parent::__construct($data);
		$this->type = HtmlTypeRoot;
	}
	
	public function getHtml() {
		return parent::getHtml();
	}

};

?>